24 — 环境贴图
介绍
我们在之前的一节课已经谈过环境贴图了。
就是那些环绕场景的图片,它们可以作为背景,也可以直接用在物体上作为反射和光照。是的,你没看错,环境贴图可以用一种非常逼真的方式照亮整个场景。
这节课,我们会探索环境贴图的不同格式,以及实现它们的各种技术。
我们还会探索如何使用像 Blender 和人工智能驱动的图像生成器这样的资源来查找和生成那些环境贴图。
设置
我们要用一个带很多细节和材质变化的写实模型,来欣赏我们要测试的不同环境贴图。我们会用来自 GLTF 样本模型仓库的 Flight Helmet,你能在 /static/models/ 文件夹里找到它。
目前,我们场景里只有一个白色的环面结(torus knot)和一个 lil-gui 实例。
模型
让我们加载我们的模型。
首先,实例化 GLTFLoader。我们会在 import 之后立刻把不同的 loader 归到一起。这样做的主要原因是为了能把东西归组在一起:
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js'
/**
* Loaders
*/
const gltfLoader = new GLTFLoader() 我们不需要 DRACOLoader,因为这个模型没有压缩。但如果你加载一个 Draco 压缩的模型,就像我们在之前一节课做的那样实例化 DRACOLoader。
我们现在可以加载位于 /static/models/FlightHelmet/glTF/FlightHelmet.gltf 的模型了:
/**
* Models
*/
gltfLoader.load(
'/models/FlightHelmet/glTF/FlightHelmet.gltf',
(gltf) =>
{
console.log('success')
console.log(gltf)
}
)
和往常一样,慢慢来,确保模型加载良好、没有任何错误。
因为它是一个复杂的模型,我们会简单地把 gltf.scene 组添加到我们自己的 scene:
gltfLoader.load(
'/models/FlightHelmet/glTF/FlightHelmet.gltf',
(gltf) =>
{
scene.add(gltf.scene)
}
) 看不到它?把相机移到白色环面结下面,你应该能看到头盔的轮廓。
模型太小了,而且完全是黑的,因为它的材质是 MeshStandardMaterial 实例,而那些需要灯光。
我们马上会添加光照,但先让模型变大一点:
gltfLoader.load(
'/models/FlightHelmet/glTF/FlightHelmet.gltf',
(gltf) =>
{
gltf.scene.scale.set(10, 10, 10)
scene.add(gltf.scene)
}
)
立方体贴图环境贴图
首先,我们会用立方体贴图。我们其实在之前的一节课里已经做过了,但练习总是好的,而且这会是一个很好的提醒。
加载纹理
/static/environmentMaps/ 文件夹里有多个环境贴图纹理。
0/、1/ 和 2/ 文件夹里包含取自 https://polyhaven.com 的 HDRI 部分的环境贴图,它们已经用 https://matheowis.github.io/HDRI-to-CubeMap/ 转换成了立方体贴图。
我们会用 0/ 文件夹里的第一个。
因为这些纹理由 6 张图片组成(像立方体的各个面),我们必须用一个 CubeTextureLoader。
把 CubeTextureLoader 添加到我们的 loaders:
/**
* Loaders
*/
// ...
const cubeTextureLoader = new THREE.CubeTextureLoader() 现在我们可以加载纹理了。顺序是 positive x、negative x、positive y、negative y、positive z 和 negative z。
在创建 scene 之后添加这些参数:
/**
* Environment map
*/
// LDR cube texture
const environmentMap = cubeTextureLoader.load([
'/environmentMaps/0/px.png',
'/environmentMaps/0/nx.png',
'/environmentMaps/0/py.png',
'/environmentMaps/0/ny.png',
'/environmentMaps/0/pz.png',
'/environmentMaps/0/nz.png'
]) 什么都不会变,因为我们加载了环境贴图但还没用它。
检查日志看有没有潜在错误。
把环境贴图作为背景
要把环境贴图加到我们场景的背景,我们可以围绕场景创建一个巨大的立方体,把它的面设为从内部可见,并把纹理应用到它上。它应该能工作而且看起来还行,但它会非常受限,只能当背景。
相反,让我们用 Three.js 的特性。
要把环境贴图应用到一个场景,把 environmentMap 赋值给 Scene 的 background 属性。确保在你先创建了 environmentMap 和 scene 之后再做这个:
scene.background = environmentMap
现在我们能分辨出模型了,让我们把环面结移到一边:
const torusKnot = new THREE.Mesh(
new THREE.TorusKnotGeometry(1, 0.4, 100, 16),
new THREE.MeshBasicMaterial()
)
torusKnot.position.x = - 4
torusKnot.position.y = 4
scene.add(torusKnot)
并把环面结的材质改成 MeshStandardMaterial,这样我们就能在一个平滑表面上欣赏环境贴图。用以下设置:
const torusKnot = new THREE.Mesh(
new THREE.TorusKnotGeometry(1, 0.4, 100, 16),
new THREE.MeshStandardMaterial({ roughness: 0.3, metalness: 1, color: 0xaaaaaa })
)
torusKnot.position.x = - 4
torusKnot.position.y = 4
scene.add(torusKnot)
用环境贴图照亮模型
获得逼真渲染的一个必要特性是用我们的环境贴图来照亮模型。
在之前的一节课里,我们已经覆盖了如何用 envMap 属性把环境贴图应用到一个 MeshStandardMaterial。
要把环境贴图作为光照应用到整个场景,我们可以把它赋值给 scene 的 environment 属性(和 background 一样):
scene.environment = environmentMap
scene.background = environmentMap
就这些了。我们终于能看到我们的模型了。看看我们实现的漂亮又逼真的光照。反射、光照和阴影与环境贴图里描绘的完美匹配。
你可以用另外两个环境贴图测试,看看光照有多不同:
const environmentMap = cubeTextureLoader.load([
'/environmentMaps/1/px.png',
'/environmentMaps/1/nx.png',
'/environmentMaps/1/py.png',
'/environmentMaps/1/ny.png',
'/environmentMaps/1/pz.png',
'/environmentMaps/1/nz.png'
])
或者:
const environmentMap = cubeTextureLoader.load([
'/environmentMaps/2/px.png',
'/environmentMaps/2/nx.png',
'/environmentMaps/2/py.png',
'/environmentMaps/2/ny.png',
'/environmentMaps/2/pz.png',
'/environmentMaps/2/nz.png'
])
让我们回到第一个环境贴图:
const environmentMap = cubeTextureLoader.load([
'/environmentMaps/0/px.png',
'/environmentMaps/0/nx.png',
'/environmentMaps/0/py.png',
'/environmentMaps/0/ny.png',
'/environmentMaps/0/pz.png',
'/environmentMaps/0/nz.png'
])
环境属性与调整
强度
它生效了,但看起来有点暗。我们想稍微作弊一下,控制环境贴图的强度。
我们可以直接在 Scene 上用 environmentIntensity 属性做到。
在 Environment map 部分的开头,把 scene.environmentIntensity 设为 4:
/**
* Environment map
*/
scene.environmentIntensity = 4
让我们玩玩,把它加到我们的 lil-gui:
scene.environmentIntensity = 4
gui.add(scene, 'environmentIntensity').min(0).max(10).step(0.001)
现在,让我们放回 1。我们稍后会改一点
scene.environmentIntensity = 1
背景模糊度与背景强度
environmentIntensity 是个好的开始,但 Scene 还有更多。
backgroundBlurriness,会让背景模糊。我们可以在设置实际环境贴图之前放它:
scene.environmentIntensity = 1
scene.backgroundBlurriness = 0.2
如果环境分辨率相当低,或者你想让用户专注于前景的物体,这个技术就很有用。
然后还有 backgroundIntensity,它会控制背景亮度:
scene.environmentIntensity = 1
scene.backgroundBlurriness = 0.2
scene.backgroundIntensity = 5
小心,因为这个属性只控制背景,不控制环境贴图强度。
和往常一样,我们可以把这些属性加到我们的 lil-gui:
scene.environmentIntensity = 1
scene.backgroundBlurriness = 0
scene.backgroundIntensity = 1
gui.add(scene, 'environmentIntensity').min(0).max(10).step(0.001)
gui.add(scene, 'backgroundBlurriness').min(0).max(1).step(0.001)
gui.add(scene, 'backgroundIntensity').min(0).max(10).step(0.001)
现在,让我们坚守默认值。我推荐把 backgroundBlurriness 设为 0,backgroundIntensity 设为 1:
scene.environmentIntensity = 1
scene.backgroundBlurriness = 0
scene.backgroundIntensity = 1
环境与背景旋转
最后,我们可以用 backgroundRotation 和 environmentRotation 属性分别控制环境和背景的旋转。
scene.backgroundRotation.x = 1
scene.environmentRotation.x = 2
这些属性是 Euler,能在所有三个轴上旋转。但在写实环境的情况下,你通常想让地板保持在下面,这就是为什么我们会避免改变 y 以外的轴。
移除或注释掉两个旋转:
// scene.backgroundRotation.x = 1
// scene.environmentRotation.x = 2 那不该阻止我们为 y 轴添加调整来玩一玩:
gui.add(scene, 'environmentIntensity').min(0).max(10).step(0.001)
gui.add(scene, 'backgroundBlurriness').min(0).max(1).step(0.001)
gui.add(scene, 'backgroundIntensity').min(0).max(10).step(0.001)
gui.add(scene.backgroundRotation, 'y').min(0).max(Math.PI * 2).step(0.001).name('backgroundRotationY')
gui.add(scene.environmentRotation, 'y').min(0).max(Math.PI * 2).step(0.001).name('environmentRotationY')
注意我们为那些调整添加了名字,这样默认的就不是 y 了。
HDRI 等距柱状环境贴图
如果你在 /static/environmentMaps/ 文件夹里查看过,你可能注意到了那些 .hdr 文件。
点击 /static/environmentMaps/0/2k.hdr,如果你的操作系统支持,你会得到那个环境贴图的预览(如果你没有得到预览,别担心,这并不意味着你不能在 Three.js 里用它)。
这个文件有两个具体特性。
HDR
首先,文件扩展名是 .hdr。HDR 代表"High Dynamic Range"(高动态范围)(我们常说"HDRI",其中"I"代表"Image")。它意味着存储的颜色值比传统图像有高得多的范围,这使它非常适合存储亮度数据。
举个例子,如果你看一张明亮台灯的照片,它会看起来很白;如果你看一张太阳在后面的照片,它也会看起来很白。但它们的亮度数据应该不同,因为太阳实际上比台灯亮得多。HDR 图像会让这个区别清晰,而传统图像(比如 JPG 或 PNG)不会。
等距柱状
其次,如果你查看预览,你会注意到它只是一张图片,包含一种周围环境的 360° 视图。
这张图不只是水平的,因为我们也能看到天空和地板,尽管这些部分被拉伸了。
这种投影的恰当名字是"equirectangular"(等距柱状)。
注意 HDR 环境贴图不一定是等距柱状的,但通常是这样,而且 Three.js(像大多数 3D 软件包、库和引擎一样)支持这种投影。
加载和使用 HDRI
让我们从注释掉旧的环境贴图开始(同时,保留 backgroundBlurriness 和 backgroundIntensity 调整):
// // LDR cube texture
// const environmentMap = cubeTextureLoader.load([
// '/environmentMaps/0/px.jpg',
// '/environmentMaps/0/nx.jpg',
// '/environmentMaps/0/py.jpg',
// '/environmentMaps/0/ny.jpg',
// '/environmentMaps/0/pz.jpg',
// '/environmentMaps/0/nz.jpg'
// ])
// scene.background = environmentMap
// scene.environment = environmentMap
我们会注释掉每一个环境贴图实现版本,这样你只要取消注释那一段就能回到它们。
接下来,我们需要用 RGBELoader。
像我们导入 GLTFLoader 那样导入 RGBELoader:
import { RGBELoader } from 'three/examples/jsm/loaders/RGBELoader.js' "RGBE"代表"Red Green Blue Exponent"(红绿蓝指数),其中指数存储亮度。RGBE 就是我们所说的"HDR"格式的编码,这解释了 loader 的名字为什么是它而不是 HDRLoader。
用其他 loader 一起实例化它:
/**
* Loaders
*/
// ...
const rgbeLoader = new RGBELoader() 用它加载 /environmentMaps/0/2k.hdr 文件:
// HDR (RGBE) equirectangular
rgbeLoader.load('/environmentMaps/0/2k.hdr', (environmentMap) =>
{
console.log(environmentMap)
})
在回调函数里,把它的 mapping 属性设为 THREE.EquirectangularReflectionMapping(因为如之前提到的,它是一个等距柱状纹理),并把它赋值给 scene 的 background 和 environment 属性:
rgbeLoader.load('/environmentMaps/0/2k.hdr', (environmentMap) =>
{
environmentMap.mapping = THREE.EquirectangularReflectionMapping
scene.background = environmentMap
scene.environment = environmentMap
})
场景感觉更亮,特征对比更强,产生了更逼真的渲染。
缺点
虽然那些 HDR 纹理听起来很诱人,但它们通常加载和渲染起来重得多,这是需要注意的事情。
你可以通过用较低的分辨率并模糊背景来缓解这一点,这样用户就不会注意到低分辨率。
用 Blender 生成环境贴图
正如我们之前看到的,我们能从 https://polyhaven.com 的 HDRI 部分得到一堆漂亮的 HDR 环境贴图,还有其他地方能得到很酷的环境贴图。但有时候,我们需要创建一个非常具体的环境贴图,其中所有东西都正好在它应该在的地方。
幸运的是,我们现在知道怎么用 Blender(至少基础),而且它是这项工作的完美工具。
下面这部分是用 Blender 3.5.1 录制的。你可以用更新的版本,但要预期一些差异。你可以点击这个链接 https://www.blender.org/download/previous-versions/ 下载我用的同一个版本。
注意,虽然我会提醒你快捷键,但这节课假设你跟着之前学 Blender 的课程做过。而且别忘了快捷键是区域敏感的,意味着你按快捷键的时候需要悬停在与我所做的相同的区域上方。
项目设置
我们会从设置项目开始。首先,启动 Blender。
清空
删除场景里的一切。
按 A 全选,按 X 删除,然后点击提示来确认:
渲染引擎
在 Render Properties 标签页里,把 Render Engine 切换到 Cycles:
Eevee 本可以胜任,但让我们选最逼真的渲染引擎。
采样
还在 Render Properties 标签页里,在 Sampling 部分,把 Viewport 和 Render 两者的 Max Samples 设为 256:
这会让渲染快得多。随意根据你的电脑和你想等渲染完成多久来调整这个。
背景
在 World Properties 标签页里,在 Surface 部分,把 Color 设为全黑:
在这里设一个颜色会在到处应用一种昏暗的光,这可能有趣,但在我们的情况里不需要。
分辨率
在 Output Properties 标签页里,在 Format 部分,把分辨率设为 2048x1024:
虽然 WebGL 2(当前的版本)支持非 2 的幂次方纹理,但这被认为是好习惯。这样,我们就能在其他软件里使用这个环境贴图。
和采样值类似,你可以根据需要调整这个值。为了省时间,你可以改变 % 输入,设为 50%、200%,或者任何适合你的值。
基础物体
接下来,在场景中心周围创建几个物体。这里的目标是对相机看到的东西有一个快速预览。我们稍后会在场景上工作。
按 SHIFT + A 选择你想创建的物体,G 移动它,S 缩放它,R 旋转它。
在场景中心周围的每一侧放一个不同的物体:
相机
我们现在可以添加一个新相机。
按 SHIFT + A 并选择 Camera:
用 N 显示侧边栏(如果它还不可见):
当相机被选中时,从 item 标签页重置 Location 和 Rotation。
然后把 X 轴上的旋转设为 90°:
在 Object Data Properties 标签页里,在 Lens 部分,把 Type 设为 Panoramic,对于 Panorama Type,选择 Equirectangular:
顺便说一句,别指望能在 Viewport 里看到全景视图的预览。你只能在渲染时看到这种投影。
第一盏灯
按 SHIFT + A 并在 Light 菜单里创建一个 Area 灯:
把它移离中心,把它变大,并把它朝中心旋转(确切位置不重要):
在 Object Data Properties 标签页里,在 Light 部分,把它的强度设为 1000W:
在 Object Properties 标签页里,在 Visibility 部分,勾选 Camera 复选框来确保光源可见:
第一次渲染
是时候渲染场景了。
按 F12,稍等渲染完成,看看你得到什么:
正如你看到的,我们得到一个漂亮的等距柱状渲染。
保存为 HDR
当悬停在渲染上方时按 ALT + S。
在底部选一个名字。我们会用 blender-2k.hdr。
对于 File Format,选择 Radiance HDR。
最后把它保存到 /static/environmentMaps/ 文件夹。
实现
虽然它不是一个非常有趣的环境贴图,但我们能在我们的 Three.js 项目里试试它。
把我们已加载的那个的路径改成 /environmentMaps/blender-2k.hdr:
rgbeLoader.load('/environmentMaps/blender-2k.hdr', (environmentMap) =>
{
// ...
})
它生效了!
影棚灯光
我们可以创建一个带有酷炫效果、建筑、行星等等的完整场景。但那会花相当长的时间。相反,我们要创建一个简单但看起来超棒的影棚设置。
回到 Blender:选中所有物体(不是灯光)并按 X 删除它们。
我们要把灯光复制两次,把一盏光源设为蓝色,另一盏设为橙色,并把它们放在场景周围。
自己试试这个。用 SHIFT + D 复制,确保灯光朝向中心,并把第二盏的颜色设为橙色、第三盏的颜色设为蓝色。
用 F12 再次渲染。
看起来很无聊?等到你看到它在 Three.js 里的结果。
用 ALT + S 再次保存,用和之前相同的设置(Radiance HDR 和 studio-2k.hdr),替换之前的文件并重新加载页面。
既然这张图很无聊,你可以停用背景,这样我们就只享受光照:
rgbeLoader.load('/environmentMaps/studio-2k.hdr', (environmentMap) =>
{
environmentMap.mapping = THREE.EquirectangularReflectionMapping
// scene.background = environmentMap
scene.environment = environmentMap
})
别忘了把你的 Blender 文件保存到某处。
用 NVIDIA Canvas 生成 AI 环境
如果我们能直接让 AI 为我们生成一个环境贴图呢?
我们要用的第一个解决方案是 NVIDIA Canvas。
我录制这节课的时候,这个软件还在 beta 阶段,而且只在 Windows 上运行,这就是为什么我们不会在它上面花太多时间。
如果你没有 Windows,你可以出于好奇跟着看,但留下来,因为紧接着还有另一个 AI 工具。
设置
首先,从网站 https://www.nvidia.com/en-us/studio/canvas/ 下载 NVIDIA Canvas
像任何软件包一样安装它,然后打开 NVIDIA Canvas 应用。
打开时,你能在"standard"(标准)图像和"panorama"(全景)之间选择。选全景:
NVIDIA Canvas 背后的思路是,你用对应视觉元素(云、草、河流、树等等)的特定颜色粗略地画出形状,软件会用 AI 生成一个逼真的版本。
材质
在屏幕右侧,有一个 Materials 部分,包含可用的颜色/元素。
如果你想得到材质的名字,就悬停在它们上面。
到这一步,你可以选一个材质并开始画:
工具
在屏幕左侧,你能把你的工具改成画线、擦除、拾取材质、填充、改变笔刷大小等等。
样式
在 Materials 下面,有一个 Styles 部分,你能在那里改变生成的全景的样式。
而如果你有点喜欢这个样式,但想测试一个变体,就用 Variation 滑块:
提示
记住,因为它是等距柱状投影,画布顶部和底部的部分正好在相机的上方和下方。不要在那里添加太多细节:
你可以直接在生成的全景上画:
你也可以使用像任何 2D 软件工具一样工作的图层。
但因为我们只是在快速起草一个粗略版本,我们往往会忘记自己在哪个图层里,这意味着那个特性不是很有用。
导出
当你对结果满意时(别太挑剔),点击导出按钮,把它命名为 nvidiaCanvas-4k.exr,并保存到 /static/environmentMaps/ 文件夹(替换已存在的文件)。
实现
对于不能用 NVIDIA Canvas 的人,生成的环境贴图已经在 /static/environmentMaps/nvidiaCanvas-4k.exr 里可用了。
你可能注意到了扩展名是 .exr 而不是 .hdr。
我们导出的文件就存储的颜色范围而言也是一张"HDR"图像,但编码不同。EXR 也能存储图层,而且有一个 alpha 通道。
在我们的情况里,它没有造成多大区别,但我们仍然需要用一个不同的 loader。
从注释掉 HDR(RGBE)版本开始:
// // HDR (RGBE) equirectangular
// rgbeLoader.load('/environmentMaps/blender-2k.hdr', (environmentMap) =>
// {
// environmentMap.mapping = THREE.EquirectangularReflectionMapping
// // scene.background = environmentMap
// scene.environment = environmentMap
// })
然后导入 EXRLoader 并像其他 loader 那样实例化它:
import { EXRLoader } from 'three/addons/loaders/EXRLoader.js'
/**
* Loaders
*/
// ...
const exrLoader = new EXRLoader() 用它加载 '/environmentMaps/nvidiaCanvas-4k.exr' 文件,把 mapping 设为 THREE.EquirectangularReflectionMapping,并把它应用到 background 和 environment 两者:
// HDR (EXR) equirectangular
exrLoader.load('/environmentMaps/nvidiaCanvas-4k.exr', (environmentMap) =>
{
environmentMap.mapping = THREE.EquirectangularReflectionMapping
scene.background = environmentMap
scene.environment = environmentMap
})
想法
NVIDIA Canvas 仍在 beta 阶段,这个工具缺少像 HDR(RGBE)导出、灯光管理、选择分辨率、能画的不只是风景等等重要特性。另一方面,它真的有效,令人印象深刻,值得关注。
在 Twitter 上关注 @NVIDIAStudio。
用 BlockadeLabs 生成 AI 环境贴图
让我们继续我们的 AI 之旅,用 BlockadeLabs 的 Skybox Lab 来生成环境贴图。
我必须提醒你,你在屏幕上看到的可能与我录制这节课时想出来的不同。这个工具变化非常快,特性也在定期添加。
打开 Skybox Lab。因为它是一个网站,所以这个应该在每个设备上都能用。
开箱即用,你已经得到一个看起来不错的环境贴图。你可以拖放来移动相机,用鼠标滚轮来放大缩小。
生成
如果你用过 AI,你大概熟悉提示词(prompt)的概念。在页面底部,有一个文本输入框,你能在那里让 AI 生成某些东西。而那个东西只受你的想象力限制。
注意我用的提示词很可能为你产生不同的输出,因为 AI 每次都会生成一个随机的场景。
在文本输入的右边,有一个选择输入,你能在那里选择样式。
去吧,玩一玩,看看 AI 想出什么。你可以要求物体、颜色、地点、一天中的时间等等。
生成环境贴图可能花相当长的时间。这取决于服务器以及它们有多忙,但你通常要等 30 秒到 2 分钟之间。
这里有一些提示词和样式的例子:
用 Digital Painting 样式的 neon city, orange lights, taxi:
用 Fantasy Land 样式的 castles at night:
用 Interior Views 样式的 cozy wood cabin with cauldron and potions:
用 SciFi 样式的 white sky scrapers in clouds at day time:
用 Anime Art Style 样式的 japan streets with cherry blossom in summer, high contrast:
下载
一旦你对结果满意(再一次,别太挑剔),点击 download 按钮。
一些生成的环境贴图已经在 /static/environmentMaps/blockadesLabsSkybox/ 文件夹里可用了。你也可以把你的放进去。
实现
正如你看到的,生成的环境贴图是等距柱状 LDR 图像。我希望将来能看到 HDR,但 LDR 目前也能胜任。
注释掉 HDR(EXR)等距柱状:
// // HDR (EXR) equirectangular
// exrLoader.load('/environmentMaps/nvidiaCanvas-4k.exr', (environmentMap) =>
// {
// environmentMap.mapping = THREE.EquirectangularReflectionMapping
// scene.background = environmentMap
// scene.environment = environmentMap
// })
因为它是一个简单的 .jpg 文件,我们可以用 TextureLoader。
实例化一个 TextureLoader:
/**
* Loaders
*/
// ...
const textureLoader = new THREE.TextureLoader() 用它加载环境贴图:
// LDR equirectangular
const environmentMap = textureLoader.load('/environmentMaps/blockadesLabsSkybox/anime_art_style_japan_streets_with_cherry_blossom_.jpg') 把它的 mapping 设为 THREE.EquirectangularReflectionMapping:
environmentMap.mapping = THREE.EquirectangularReflectionMapping 把它的 colorSpace 设为 THREE.SRGBColorSpace:
environmentMap.colorSpace = THREE.SRGBColorSpace 你会在后面的一节课学到更多关于色彩空间的内容。
把它赋值给 scene 的 background 和 environment 属性:
scene.background = environmentMap
scene.environment = environmentMap
因为环境贴图是 LDR 的,你可能想增加 scene 上默认的 environmentIntensity:
scene.environmentIntensity = 4
更多特性
深度
通过勾选 Generate Depth,并再次生成环境贴图,点击 download 按钮也会下载一张灰度深度图(你可能必须允许你的浏览器下载多个文件):
这可以用来创建视差效果、雾,或任何你能想到的东西。不过,我们不会用那个。
Sketch-to-skybox
在左侧,你能访问一些工具,可以直接在环境上画形状。生成的元素会尝试匹配那些形状。
你可以在这里观看这个 Sketch-to-skybox 特性的演示:https://twitter.com/BlockadeLabs/status/1659263006415659008
它有点随机,但要确保特定位置有物体可能挺有趣。
想法
Skybox Labs 生成的环境贴图绝对华丽,但你需要花些时间尝试提示词和样式来掌握窍门。
Blockade Labs 团队一直在改进这个工具,你甚至可能访问到我录制这节课时还没有的新特性。
保持好奇,在 Twitter 上关注 @blockadelabs。
地面投射环境贴图
把环境贴图用作背景时最烦人的事情之一,就是物体看起来像在飞。这是因为环境贴图在无限远处。
我们可以在下面创建一个平面,试着让它看起来、感觉起来像环境贴图的一部分,但有一个更好的解决方案,叫做"ground projected skybox"(地面投射天空盒)。
首先,注释掉 LDR 等距柱状环境贴图:
// // LDR equirectangular
// const environmentMap = textureLoader.load('/environmentMaps/blockadesLabsSkybox/interior_views_cozy_wood_cabin_with_cauldron_and_p.jpg')
// environmentMap.mapping = THREE.EquirectangularReflectionMapping
// environmentMap.colorSpace = THREE.SRGBColorSpace
// scene.background = environmentMap
// scene.environment = environmentMap
然后,让我们用这节课开头的其中一个 HDR 环境贴图,但只作为 environment(不是 background):
// Ground projected skybox
rgbeLoader.load('/environmentMaps/2/2k.hdr', (environmentMap) =>
{
environmentMap.mapping = THREE.EquirectangularReflectionMapping
scene.environment = environmentMap
})
降低 scene 上的 environmentIntensity:
scene.environmentIntensity = 1
实现
导入 GroundedSkybox 类:
import { GroundedSkybox } from 'three/addons/objects/GroundedSkybox.js' 在环境贴图的回调函数里,实例化 GroundedSkybox,把 environmentMap 作为参数传入,后面跟着 15 和 70。最后,把它添加到场景:
rgbeLoader.load('/environmentMaps/2/2k.hdr', (environmentMap) =>
{
// ...
// Skybox
const skybox = new GroundedSkybox(environmentMap, 15, 70)
scene.add(skybox)
})
你看到的作为背景的东西是一个简单的被压扁的球体。
第一个参数是我们要用的环境纹理,第二个参数是那个球体的高度,第三个参数是半径。
出于好奇,让我们通过在它的材质上激活 wireframe 来看看这个球体实际是什么样子:
rgbeLoader.load('/environmentMaps/2/2k.hdr', (environmentMap) =>
{
// ...
// Skybox
const skybox = new GroundedSkybox(environmentMap, 15, 70)
skybox.material.wireframe = true
scene.add(skybox)
})
你能看到球体的底部被压平了,但它太低了。
我们可以通过把整个 skybox 向上移到 position.y 设为球体的 height(这个情况里是 15)来修复这个:
// Ground projected skybox
rgbeLoader.load('/environmentMaps/2/2k.hdr', (environmentMap) =>
{
// ...
// Skybox
const skybox = new GroundedSkybox(environmentMap, 15, 70)
skybox.material.wireframe = true
skybox.position.y = 15
scene.add(skybox)
})
被压扁的球体的底部现在与我们的模型完美匹配。让我们停用 wireframe:
// Ground projected skybox
rgbeLoader.load('/environmentMaps/2/2k.hdr', (environmentMap) =>
{
// ...
// Skybox
const skybox = new GroundedSkybox(environmentMap, 15, 70)
// skybox.material.wireframe = true
skybox.position.y = 15
scene.add(skybox)
})
现在由你来找 radius 和 height 的完美值,让它匹配你用的环境贴图。
注意这个技巧不总是有效,尤其是如果环境贴图图像里的一些物体靠近中心的话。
实时环境贴图
我想给你展示的最后一个技术是创建一个动态环境贴图,我们在每一帧渲染场景,并在 Three.js 里直接把结果用作 environment。
设置
从注释掉地面投射天空盒开始:
// // Ground projected skybox
// rgbeLoader.load('/environmentMaps/2/2k.hdr', (environmentMap) =>
// {
// environmentMap.mapping = THREE.EquirectangularReflectionMapping
// scene.environment = environmentMap
// const skybox = new GroundProjectedSkybox(environmentMap)
// skybox.radius = 120
// skybox.height = 11
// skybox.scale.setScalar(50)
// scene.add(skybox)
// gui.add(skybox, 'radius', 1, 200, 0.1).name('skyboxRadius')
// gui.add(skybox, 'height', 1, 100, 0.1).name('skyboxHeight')
// })
这一部分会比之前的部分需要多一点代码。确保在视觉上把它与其余代码分开:
/**
* Real time environment map
*/ 我们要再次插入 LDR 木屋环境贴图,但只作为 scene 的 background:
// Base environment map
const environmentMap = textureLoader.load('/environmentMaps/blockadesLabsSkybox/interior_views_cozy_wood_cabin_with_cauldron_and_p.jpg')
environmentMap.mapping = THREE.EquirectangularReflectionMapping
environmentMap.colorSpace = THREE.SRGBColorSpace
scene.background = environmentMap
我们本可以用之前任何一个其他环境贴图,但这张看起来不错而且相当暗,这马上会派上用场。
神圣甜甜圈
我们要创建一个环绕场景的圆环(或甜甜圈),并试着让那个圆环照亮和反射在我们物体的表面上。
用一个 TorusGeometry,并把对象叫做 holyDonut,因为听起来很酷:
// Holy donut
const holyDonut = new THREE.Mesh(
new THREE.TorusGeometry(8, 0.5),
new THREE.MeshBasicMaterial({ color: 'white' })
)
holyDonut.position.y = 3.5
scene.add(holyDonut)
你可以往环境贴图里添加任何你想加的东西,但为了这节课,让我们坚持用简单的东西。
接下来,我们要让神圣甜甜圈旋转。
在 tick 函数的开头,紧接 elapsedTime 之后,我们要测试 holyDonut 是否存在,这样我们就能轻松停用那部分:
const tick = () =>
{
// Time
const elapsedTime = clock.getElapsedTime()
// Real time environment map
if(holyDonut)
{
}
// ...
} 在 rotation 上用 elapsedTime:
const tick = () =>
{
// Time
const elapsedTime = clock.getElapsedTime()
// Real time environment map
if(holyDonut)
{
holyDonut.rotation.x = elapsedTime
}
// ...
}
为了让动画稍微更有趣一点,让我们用一个 Math.sin(...) 并乘以它:
const tick = () =>
{
// Time
const elapsedTime = clock.getElapsedTime()
// Real time environment map
if(holyDonut)
{
holyDonut.rotation.x = Math.sin(elapsedTime) * 2
}
// ...
}
立方体渲染目标
主要思路是我们要在我们自己的环境贴图纹理里渲染场景,而且它会是一个立方体贴图。
要渲染到一个立方体贴图里,我们需要用一个 WebGLCubeRenderTarget。渲染目标是我们能存储任何场景渲染的纹理。
第一个参数是立方体每一面的分辨率(让我们用 256):
// Cube render target
const cubeRenderTarget = new THREE.WebGLCubeRenderTarget(256) 第二个参数是一个对象,它的属性会被用来设置渲染目标。
这里唯一重要的属性是 type,我们能在其中选择会存储的值的类型。因为我们想要和一个具有高数据范围的 HDR 相同的行为,我们应该用 THREE.HalfFloatType 或 THREE.FloatType。
Float 用 32 位来存储很宽范围的值。
HalfFloat 只用了 16 位,但它仍然是一个相当宽的范围,区别不会被注意到,而且因为它需要更少的内存,对性能更好:
const cubeRenderTarget = new THREE.WebGLCubeRenderTarget(
256,
{
type: THREE.FloatType
}
) 即使 cubeRenderTarget 里什么都还没渲染,我们也能把它赋值给 scene 的 environment:
scene.environment = cubeRenderTarget.texture
相机
因为我们需要为立方体的每一面渲染一个纹理,我们需要渲染 6 个方形纹理。我们可以用一个 PerspectiveCamera,做点数学,确保视场角完美填满一面,做 6 次渲染,然后把它们组合起来。或者我们可以用 CubeCamera,它会为我们做这些。
第一个参数是 near,第二个参数是 far,第三个参数是要把渲染保存到其中的 WebGLCubeRenderTarget:
// Cube camera
const cubeCamera = new THREE.CubeCamera(0.1, 100, cubeRenderTarget)
在每一帧渲染
回到我们的 tick function,我们现在可以通过调用它的 update 方法并传入 renderer 和 scene,来在 cubeCamera 里渲染场景:
const tick = () =>
{
// ...
// Real time environment map
if(holyDonut)
{
holyDonut.rotation.x = Math.sin(elapsedTime) * 2
cubeCamera.update(renderer, scene)
}
// ...
}
我们又得到了环境贴图照明,但这一次神圣甜甜圈是它的一部分。
因为我们在渲染目标上用了高范围纹理,我们可以让立方体的颜色超出 0 到 1 的范围。
要做到这一点,把 holyDonut 的 color 改成一个 Color 实例,我们给它传 10, 4, 2,它应该看起来很亮,但也偏黄/橙:
const holyDonut = new THREE.Mesh(
new THREE.TorusGeometry(8, 0.5),
new THREE.MeshBasicMaterial({ color: new THREE.Color(10, 4, 2) })
)
虽然很难注意到,但有一个 bug。
要看到它,把环面结的 roughness 改成 0,并观察反射:
const torusKnot = new THREE.Mesh(
// ...
new THREE.MeshStandardMaterial({ roughness: 0, metalness: 1, color: 0xaaaaaa })
)
正如你看到的,场景里所有物体现在都是环境贴图的一部分了。
这不一定是个大问题,这取决于你想在你的环境贴图里放什么,但头盔和环面结正在阻挡光线,这不太理想。
图层
要修复那个,我们要用 Layers(图层)。图层像分类一样工作,可以在任何继承自 Object3D(比如 Mesh)的对象上设置。
通过在相机上设置图层,这个相机只会看到匹配相同图层的对象。
举个例子,如果一个相机的图层设为 1 和 2,它只会看到图层设为 1 或 2 的对象。
默认情况下,所有对象和相机的图层都设为 0。
要改变一个对象或相机的图层,我们可以用 3 个方法:
object.layers.enable(...)会添加一个图层;object.layers.disable(...)会移除一个图层;object.layers.set(...)会自动启用一个图层并停用所有其他图层。
在我们的情况里,我们想要 cubeCamera 只看到 holyDonut,所以把它的图层设为 1:
const cubeCamera = new THREE.CubeCamera(0.1, 100, cubeRenderTarget)
cubeCamera.layers.set(1) 然后我们想要我们的 holyDonut 对默认相机和 cubeCamera 都可见。而且因为默认图层是 0,我们只需要添加 1:
const holyDonut = new THREE.Mesh(
new THREE.TorusGeometry(8, 0.5),
new THREE.MeshBasicMaterial({ color: new THREE.Color(10, 4, 2) })
)
holyDonut.layers.enable(1)
我们修复了 bug,cubeCamera 只渲染 holyDonut 了。
想法
对实时环境贴图要小心。在每一帧做 6 次渲染在性能方面可能相当重。这就是为什么你应该留意帧率,尽量在 WebGLCubeRenderTarget 上用尽可能小的分辨率,并保持被渲染进环境贴图的场景简单。
另外,对图层要小心。很容易搞不清什么正在被渲染。此外,注意灯光不受图层影响。
继续深入
我们完成了!你现在有了所有工具来制作美丽的环境贴图,让你的场景闪耀。
用 #threejsJourney 话题标签在 Twitter 上或在 Three.js Journey Discord 服务器上分享结果。我很想看到你的作品!